/*******************************************************************************
 * Copyright (c) 2011 Irineu Martins de Lima Moura (imlm2).
 * This file is part of JBeat.
 * 
 *     JBeat is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     JBeat is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public License
 *     along with JBeat.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Irineu Martins de Lima Moura (imlm2) - initial API and implementation
 ******************************************************************************/
package br.ufpe.cin.imlm2.jbeat.soot.report;

import static br.ufpe.cin.imlm2.util.FormattedIO.printlnTabbed;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import soot.SceneTransformer;
import soot.SootClass;
import soot.SootMethod;
import br.ufpe.cin.imlm2.jbeat.soot.common.SootClassResolver;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.PropagatesToTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.ThrowableSetTag;
import br.ufpe.cin.imlm2.jbeat.soot.throwableset.tags.TrapedSetTag;
import br.ufpe.cin.imlm2.util.Naming;
import br.ufpe.cin.imlm2.util.SystemProperties;
import br.ufpe.cin.imlm2.util.options.IOptions;
import br.ufpe.cin.imlm2.util.options.NoSuchOptionException;
import br.ufpe.cin.imlm2.util.options.Parameterizable;
import br.ufpe.cin.imlm2.util.options.ParsingException;

public class ExceptionReporter extends SceneTransformer implements Parameterizable {

	private static final String REPORT_EXTENSION = ".txt";
	private File path;
	private ReportOptions options;

	public ExceptionReporter() {
		this.options = new ReportOptions();
	}

	private void setPath( String p ) {
		p = p.trim();
		if ( !p.equals( "" ) && !p.endsWith( SystemProperties.FILE_SEPARATOR ) ) {
			p = p + SystemProperties.FILE_SEPARATOR;
		}
		this.path = this.checkPaths( p );
	}

	private File checkPaths( String path ) {
		File dir = new File( path );
		if ( !dir.exists() ) {
			dir.mkdirs();
		}
		return dir;
	}

	public void reportClasses( Collection<SootClass> classes ) {
		try {

			for ( SootClass sootClass : classes ) {
				if ( !sootClass.isInterface() ) {
					File f = getOutputFile(sootClass);
					if ( !f.exists() )
						f.createNewFile();
					Writer reporter = new FileWriter( f, false );
					reportClass( reporter, sootClass );
					reporter.flush();
					reporter.close();
				}
			}

		} catch ( IOException e ) {
			throw new RuntimeException( "Ocorreu um problema durante a escrita do relatorio", e );
		}
	}

	private File getOutputFile(SootClass sootClass) {
		if(this.options.isHierarchical()){
			String javaPackageName = sootClass.getJavaPackageName();
			String outDirHierarchy = javaPackageName.replace(".", 
					SystemProperties.FILE_SEPARATOR);
			outDirHierarchy = path.getAbsolutePath() + SystemProperties.FILE_SEPARATOR +
					outDirHierarchy;
			File f = new File(outDirHierarchy);
			if(!f.exists())
				f.mkdirs();
			f = new File(outDirHierarchy + SystemProperties.FILE_SEPARATOR +
					sootClass.getShortName() + REPORT_EXTENSION );
			return f;
		} else
			return new File( path.getAbsolutePath() + SystemProperties.FILE_SEPARATOR
				+ sootClass.getName() + REPORT_EXTENSION );
	}

	private void reportClass( Writer reporter, SootClass sootClass ) throws IOException {
		printlnTabbed( 0, sootClass, reporter );
		List<SootMethod> methods = sootClass.getMethods();
		if ( !this.options.isFull_report() )
			this.filterSyntheticMethods( methods );
		for ( SootMethod sootMethod : methods ) {
			printlnTabbed( 1, formatName( sootMethod ), reporter );
			if ( sootMethod.hasTag( ThrowableSetTag.TAG_NAME ) ) {
				ThrowableSetTag tst = (ThrowableSetTag) sootMethod
						.getTag( ThrowableSetTag.TAG_NAME );
				printlnTabbed( 2, "Throwable Set:", reporter );
				if ( !this.options.isDetailed() ) {
					for ( SootClass sc : tst.getThrowableSet() ) {
						printlnTabbed( 3, sc.getJavaStyleName(), reporter );
					}
				} else {
					Set<SootClass> generatedSet = tst.getAllGeneratedExceptions();
					Set<SootClass> thrownSet = tst.getAllThrownExceptions();
					Map<SootClass, Set<SootMethod>> propagatedMap = tst.getPropagatedMap();
					Map<SootClass, Set<SootMethod>> rethrownMap = tst.getRethrownMap();

					printlnTabbed( 3, "Generated:", reporter );
					for ( SootClass generated : generatedSet ) {
						printlnTabbed( 4, generated.getName(), reporter );
					}
					printlnTabbed( 3, "", reporter );

					printlnTabbed( 3, "Thrown:", reporter );
					for ( SootClass thrown : thrownSet ) {
						printlnTabbed( 4, thrown.getName(), reporter );
					}
					printlnTabbed( 3, "", reporter );

					printlnTabbed( 3, "Propagated:", reporter );
					printMap(reporter, propagatedMap);
					printlnTabbed( 3, "", reporter );

					printlnTabbed( 3, "Rethrown:", reporter );
					printMap(reporter, rethrownMap);

				}

			}

			if(sootMethod.hasTag(TrapedSetTag.TAG_NAME)){
				printlnTabbed( 0, "", reporter );
				printlnTabbed( 2, "Trapped set:", reporter );
				TrapedSetTag tst = (TrapedSetTag) sootMethod.getTag(TrapedSetTag.TAG_NAME);
				printMap(reporter, tst.getTrappedMap());
			}

			if(sootMethod.hasTag(PropagatesToTag.TAG_NAME)){
				printlnTabbed( 0, "", reporter );
				printlnTabbed( 2, "Propagates to set:", reporter );
				PropagatesToTag ptt = (PropagatesToTag) sootMethod.getTag(PropagatesToTag.TAG_NAME);
				Set<SootMethod> meths = ptt.getMethods();
				for (SootMethod m : meths) {
					printlnTabbed(3, m, reporter);
				}
			}

			printlnTabbed( 0, "", reporter );
		}
		printlnTabbed( 0, "", reporter );
	}

	private void printMap(Writer reporter,
			Map<SootClass, Set<SootMethod>> map) throws IOException {
		for ( SootClass propagated : map.keySet() ) {
			printlnTabbed( 4, propagated.getName() + " from ", reporter );
			for ( SootMethod source : map.get( propagated ) ) {
				printlnTabbed(
						5,
						"- "
								+ source.getSignature().replace(
										"void <init>",
										source.getDeclaringClass()
										.getShortJavaStyleName() ), reporter );
			}
		}
	}

	private String formatName( SootMethod sm ) {
		String signature = sm.getDeclaration();
		signature = signature.replace( "void <init>", sm.getDeclaringClass()
				.getShortJavaStyleName() );
		return signature;
	}

	private void filterSyntheticMethods( List<SootMethod> methods ) {
		Iterator<SootMethod> it = methods.iterator();
		while ( it.hasNext() ) {
			SootMethod next = it.next();
			if ( Naming.isMethodSynthetic( next.getName() ) )
				it.remove();
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	protected void internalTransform( String phaseName, Map opts ) {
		try {
			this.options.parse( opts );
		} catch ( NoSuchOptionException e ) {
			System.err.println( "Invalid option '" + e.getInvalidOtion()
					+ "' found, following options being ignored." );
		} catch ( ParsingException e ) {
			throw new RuntimeException( "Unable to parse phase arguments.", e );
		}

		Collection<SootClass> classes = SootClassResolver.resolveClasses(
				this.options.getReportClasses(), this.options.getReportPackages(),
				new ArrayList<String>(), new ArrayList<String>(),
				this.options.isReportStandardlibraries() );

		System.err.println( "Exception report begining at " + Calendar.getInstance().getTime() );
		this.setPath( this.options.getReport_path() );
		this.reportClasses( classes );
		System.err.println( "Exception report finished at " + Calendar.getInstance().getTime() );
	}

	@Override
	public IOptions getOptions() {
		return this.options;
	}

}
